from typing import List, Dict, Any

from flow import Flow


class VPNAnalyzer:
    """ Base Class of all the VPN analyzers """
    def __init__(self):
        self.data = None

        # This is used for the fields that are integers and need to add the integers together.
        self.fields_need_to_add = None

        self.name = "Analyzer"
        self.protocol_name = ""

    def analyze(self, flow: Flow):
        """ base method for analyzing VPN protocol information

        :param flow: flow of tcp/udp/esp/gre/l2tp stream
        :return: bool value.
        if this stream doesn't contain the certain VPN protocol, will return immediately.
        """
        if self.protocol_name not in flow.vpn_protocols:
            return

        # this is to define the fields that are used to add together
        # will be invalid if contain str list field.
        self.fields_need_to_add = []  # type: List[str]

        # it is essential to initialize self.data everytime in the self.analyze method.
        self.data = {}  # type: Dict[str, Any]

        # we need subclass to realize this method
        self.preprocess()

        for packet in flow.packets:
            try:
                analyzing_layer = getattr(packet, self.protocol_name)
            except AttributeError:
                continue

            for field in analyzing_layer.field_names:
                if len(field) == 0:
                    continue

                value = self.data.get(field, None)

                # try to transfer the attr into int, if can't, transfer into string.
                try:
                    attr = int(getattr(analyzing_layer, field))
                except ValueError:
                    attr = str(getattr(analyzing_layer, field))

                if value is not None and isinstance(value, list):  # if the value is a list
                    self.data[field].append(str(attr))  # we need to change attr into str and append it.
                elif value is not None and isinstance(value, set):  # if the value is a set
                    self.data[field].add(str(attr))  # we need to change attr into str and add it
                else:
                    self.data[field] = attr  # if the field is not defined, we need to simply set key-value into data

        for key, value in self.data.items():
            if not isinstance(value, list) and not isinstance(value, tuple) and not isinstance(value, set):
                continue

            if key in self.fields_need_to_add:
                try:
                    self.data[key] = sum(self.data[key])
                except TypeError:
                    self.data[key] = list(value)
                continue

            # changed iterable data into list
            self.data[key] = list(value)

        # we need subclass to realize this method
        self.postprocess()

        # add protocol name to data
        self.data["protocol"] = self.protocol_name
        flow.udps.field_names.append(self.protocol_name)
        setattr(flow.udps, self.protocol_name, self.data)

    def preprocess(self):
        """ preprocess func, need to be inherited from the subclasses
        if subclass analyzer needs to define types in the class's data variable, we need to inherit this func.
        On the other hand, if some types in data variable needs to be added, we need to use self.fields_need_to_add

        :return: None
        """
        pass

    def postprocess(self):
        """ postprocess func, need to be inherited from the subclasses
        if subclass analyzer needs to handle some data in the data variable, we need to inherit the func to change
        the type or change the value of the data.

        :return: None
        """
        pass


class PPTPAnalyzer(VPNAnalyzer):
    """ VPN Analyzer for Point-to-Point Tunneling Protocol """
    def __init__(self):
        super().__init__()

        self.name = "PPTP Analyzer"
        self.protocol_name = "pptp"

    def preprocess(self):
        # we need to define the length as int value first, because we need to add all the packets' length altogether.
        self.data = {
            "length": 0,
            "control_message_type": set(),
            "magic_cookie": "",
            "protocol_version": 0,
            "framing_capabilities": [],
            "bearer_capabilities": [],
            "maximum_channels": 0,
            "firmware_revision": 0,
            "host_name": set(),
            "vendor_name": set(),
            "call_id": set(),
            "call_serial_number": 0,
            "minimum_bps": 0,
            "maximum_bps": 0,
            "bearer_type": 0,
            "framing_type": 0,
            "packet_receive_window_size": [],
            "packet_processing_delay": 0,
            "phone_number_length": 0,
            "phone_number": 0,
            "subaddress": "",
            "peer_call_id": 0,
            "connect_speed": 0,
            "physical_channel_id": [],
            "send_accm": "",
            "receive_accm": "",
            "identifier": [],
            "reason": [],
        }
        self.fields_need_to_add = ["length"]

    def postprocess(self):
        # change the sub-address key as str value
        if self.data.get("subaddress", None) is not None:
            self.data["subaddress"] = str(self.data["subaddress"])


class GREAnalyzer(VPNAnalyzer):
    """ VPN Analyzer for Generic Routing Encapsulation Protocol """
    def __init__(self):
        super().__init__()

        self.name = "GRE Analyzer"
        self.protocol_name = "gre"

    def preprocess(self):
        self.data = {
            "flags_and_version": "",
            "key_payload_length": 0,
            "key_call_id": [],
        }
        self.fields_need_to_add = ["key_payload_length"]


class L2TPAnalyzer(VPNAnalyzer):
    """ VPN Analyzer for Layer 2 Tunneling Protocol """
    def __init__(self):
        super().__init__()

        self.name = "L2TP Analyzer"
        self.protocol_name = "l2tp"

    def preprocess(self):
        self.data = {
            "type": set(),
            "length": 0,
            "length_bit": set(),
            "seq_bit": set(),
            "offset_bit": [],
            "priority": set(),
            "version": 0,
            "tunnel": set(),
            "session": set(),
            "ns": set(),
            "nr": set(),
            "offset": [],
            "offset_padding": [],
            "avp_analog_access_supported": [],
            "avp_assigned_tunnel_id": [],
            "avp_async_framing_supported": [],
            "avp_digital_access_supported": [],
            "avp_firmware_revision": [],
            "avp_hidden": [],
            "avp_host_name": [],
            "avp_length": [],
            "avp_mandatory": [],
            "avp_message_type": [],
            "avp_protocol_revision": [],
            "avp_protocol_version": [],
            "avp_receive_window_size": [],
            "avp_sync_framing_supported": [],
            "avp_type": [],
            "avp_vendor_id": [],
            "avp_vendor_name": [],
        }
        self.fields_need_to_add = ["length"]

    def postprocess(self):
        # changed the key into another
        self.data["tunnel_id"] = self.data["tunnel"]
        self.data["offset_size"] = self.data["offset"]
        self.data["session_id"] = self.data["session"]

        del self.data["tunnel"]
        del self.data["offset"]
        del self.data["session"]


class PPPAnalyzer(VPNAnalyzer):
    """ VPN Analyzer for Point to Point Protocol """
    def __init__(self):
        super().__init__()

        self.name = "PPP Analyzer"
        self.protocol_name = "ppp"

    def preprocess(self):
        self.data = {
            "protocol": []  # type: List[str]
        }

    def postprocess(self):
        # no use to del the protocol key, because later the protocol's value will be changed.
        self.data["ppp.protocol"] = self.data["protocol"]
        del self.data["protocol"]


class OpenVPNAnalyzer(VPNAnalyzer):
    """ VPN Analyzer for OpenVPN Protocol """
    def __init__(self):
        super().__init__()

        self.name = "OpenVPN Analyzer"
        self.protocol_name = "openvpn"

    def preprocess(self):
        self.data = {
            "type": set(),
            "keyid": [],
            "opcode": [],
            "sessionid": [],
            "hmac": [],
            "pid": [],
            "net_time": [],
            "mpidarraylength": [],
            "mpid": [],
            "mpidarrayelement": [],
            "rsessionid": [],
            "fragment_bytes": [],
            "fragments": [],
            "fragment": 0,
            "fragment_count": 0,
            "reassembled_length": 0,
            "data": [],
        }
        self.fields_need_to_add = ["fragment", "fragment_count", "reassembled_length"]
